# coding: utf-8

import math
import numpy as np
from scipy import optimize as sco
from quant_researcher.quant.project_tool.logger.my_logger import LOG
from quant_researcher.quant.project_tool.conf import LOCAL_CONF
from quant_researcher.quant.project_tool import hammer


def fit_with_constraints_by_array(x_s, **kwargs):
    """
    一个看起来比较方便的拟合函数，省去的是初始化和一些简单的处理重复写代码的时间

    :param array x_s: X
    :param kwargs:
        - y_s，                   array，Y，                             默认：None
        - col_count，             int，矩阵的有多少列，                   默认：None
        - target_func，           function，需要 minimize 的目标函数，    默认：None
        - with_intercept，        bool，是不是带截距项的拟合，             默认：False
        - cpt_residual_mean，     bool，是不是计算残差项的均值，           默认：False
        - cpt_residual_std，      bool，是不是计算残差项的标准差，         默认：False
        - give_me_residuals，     bool，是不是需要返回残差项序列，         默认：False
        - cpt_r_square，          bool，是不是计算 R²，                   默认：False
        - no_bounds，             bool，是不是需要给每一个变量设置区间，    默认：False
        - coeffi_bounds,                    dict, 系数范围，例如：{'x1':(-1,2),'x2':(1,1)}
        - set_constraints，       bool，是不是需要设置约束条件，           默认：True
        - constraints，           约束条件，需要设置的约束条件，           默认：True
        - tol，float，            迭代结束的参考，                        默认：0.00000000001
        - method，                str，优化使用的方法，                   默认：SLSQP
        - use_weighted，          bool，是否采用加权，离现在越近权重越大，   默认：False
    :return: dict
        +---------------+--------------------------------------+
        |       键      |                  值                  |
        +---------------+--------------------------------------+
        |   intercept   |       -1.2181884498547816e-08        |
        |     weight    | [[0.10563218 0.45424721 0.4401206 ]] |
        |   residuals   |          [[-2.15518825e-08]          |
        |               |          [ 6.74813894e-09]]          |
        |  residual_std |        9.103898990171573e-09         |
        | residual_mean |        -8.935719453262436e-09        |
        |    r_square   |          0.9999999999999898          |
        +---------------+--------------------------------------+
    """
    y_s = kwargs.pop('y_s', None)
    col_count = kwargs.pop('col_count', None)
    target_func = kwargs.pop('target_func', None)
    with_intercept = kwargs.pop('with_intercept', False)
    cpt_residual_mean = kwargs.pop('cpt_residual_mean', False)
    cpt_residual_std = kwargs.pop('cpt_residual_std', False)
    give_me_residuals = kwargs.pop('give_me_residuals', False)
    no_bounds = kwargs.pop('no_bounds', False)
    use_weighted = kwargs.pop('use_weighted', False)
    x0 = kwargs.pop('x0', None)

    # 拟合优度（Goodness of Fit）是指回归直线对观测值的拟合程度。度量拟合优度的统计量是可决系数（亦称确定系数）R²
    # R²最大值为1。R²的值越接近1，说明回归直线对观测值的拟合程度越好；反之，越差
    cpt_r_square = kwargs.pop('cpt_r_square', False)

    set_constraints = kwargs.pop('set_constraints', True)
    coeffi_bounds = kwargs.pop('coeffi_bounds', None)
    constraints = kwargs.pop('constraints', None)  # 如果没有，只要需要设置 set_constraints 为 True，后面会自动加上
    tol = kwargs.pop('tol', 0.00000000001)  # 可选：None
    method = kwargs.pop('method', 'SLSQP')  # SLSQP：序列最小二乘法；COBYLA；L-BFGS-B：据说是全局最小值算法

    msg = f'优化方法：{method}；\n' \
        f'结束惩罚：{tol}；\n' \
        f'设置约束：{set_constraints}；\n' \
        f'约束：{constraints}；\n' \
        f'计算R方：{cpt_r_square}；\n' \
        f'计算残差项均值：{cpt_residual_mean}；\n' \
        f'计算残差项标准差：{cpt_residual_std}；\n'

    row_count = None
    if x_s is not None:
        x_array = np.array(x_s)
        # show(x_s, name='X')
        if len(x_array.shape) == 1:
            hammer.slim(x_array, name='X矩阵的shape的长度不能为1')
            raise RuntimeError(f'X矩阵的shape的长度不能为1')
        row_count, col_count = x_array.shape
        msg = f'⭐X({row_count}×{col_count})⭐'

    y_row_count = None
    y_col_count = None
    if y_s is not None:
        y_array = np.array(y_s)
        # show(y_s, name='Y')
        if len(y_array.shape) == 1:
            raise RuntimeError(f'X矩阵的shape的长度不能为1')
        y_row_count, y_col_count = y_array.shape
        msg = f'⭐Y({y_row_count}×{y_col_count})⭐'

    if x_s is not None and y_s is not None:
        if row_count != y_row_count:
            hammer.slim(x_s, name='引起错误的X')
            hammer.slim(y_s, name='引起错误的Y')
            raise ValueError('俩矩阵行数不一样')
        if y_col_count != 1:
            raise ValueError('Y矩阵列数不为：1')

    if use_weighted:
        # 半衰期设为回归窗口的天数，即第一天的权重为最后一天权重的1/2
        exp_array = np.array(list(range(row_count-1, -1, -1)))
        base_array = [np.power(2, -1 / row_count)] * row_count
        w_array = np.power(base_array, exp_array)
        w_array = w_array.reshape((-1, 1))
        w_array = np.sqrt(w_array)
    else:
        w_array = np.ones([row_count, 1])

    def get_z_array(alpha_and_betas):
        alpha = alpha_and_betas[0] if with_intercept else 0
        betas = alpha_and_betas[1:] if with_intercept else alpha_and_betas
        alpha = np.array([[alpha] for _ in range(row_count)])
        betas = np.array([[ii] for ii in betas])
        # show(alpha, name='alpha')
        # show(betas, name='betas')
        # show('sum of all betas: %s' % np.sum(betas))
        z_array = (alpha + np.dot(x_array, betas) - y_array) * w_array  # 误差
        # hammer.slim(f'残差项的shape：{z_array.shape}')
        return z_array

    def get_z(z_array):
        return sum([ii[0] * ii[0] for ii in z_array])  # # 误差平方和

    def own_target_func(alpha_and_betas):
        z_array = get_z_array(alpha_and_betas)
        # show(z_array, name='Z')
        z = get_z(z_array)
        # show('z: %s' % z)
        return z

    # 权重限制，如果不需要截距项，则没有 α
    # 这里求解的是 α和 β，约束只针对β，那么组成的未知数数组为：（α，β1，β2，β3，...，β4）
    how_many_bounds = col_count + 1 if with_intercept else col_count
    if no_bounds:
        bounds = tuple((-np.Inf, np.Inf) for _ in range(how_many_bounds))
    else:
        if coeffi_bounds is not None:
            bounds = list((-np.Inf, np.Inf) for _ in range(how_many_bounds))
            for beta in coeffi_bounds.keys():
                if beta in x_s.columns:
                    if with_intercept:
                        beta_loc = x_s.columns.get_loc(beta)+1
                    else:
                        beta_loc = x_s.columns.get_loc(beta)
                    bounds[beta_loc] = coeffi_bounds[beta]
                else:
                    raise ValueError(f'未在自变量中找到{beta}变量，请确认变量名称正确')
            bounds = tuple(bounds)
        else:
            if with_intercept:
                bounds = tuple((0, 1) if idx != 0 else (-np.Inf, np.Inf) for idx in range(how_many_bounds))
            else:
                bounds = tuple((0, 1) for _ in range(how_many_bounds))
    # hammer.slim(list(bounds), name='每一个变量的取值区间')

    # Equality constraint means that the constraint function result is to be zero
    # whereas inequality means that it is to be non-negative. c >= 0
    # Note that COBYLA only supports inequality constraints.
    if constraints is None and set_constraints:
        if with_intercept:
            constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x[1:]) - 1})  # 除第一项之外權重之和 為1
        else:
            constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})  # 權重之和 為1
    # constraints = ({'type': 'ineq', 'fun': lambda x: 0.99999 - np.sum(x[1:])},  # 權重之和 小于1)

    if x0 is None:  # 如果没有传入优化初始值
        x0 = np.array(how_many_bounds * [1. / how_many_bounds, ])

    # args = ?  # tuple
    # optional Extra arguments passed to the objective function and its derivatives (`fun`, `jac` and `hess` functions)

    minimize_info = {'bounds': bounds, 'tol': tol, 'method': method, 'x0': x0}
    if constraints is not None:
        minimize_info.update({'constraints': constraints})
    # hammer.slim(minimize_info, a_f=1, name='minimize_info')
    opt_result = sco.minimize(fun=target_func if target_func else own_target_func, **minimize_info)

    # hammer.slim(opt_result, a_f=1, name='sco.minimize的返回')
    if opt_result['success']:
        fitting_weight = opt_result['x']
        # hammer.slim(f'X:{x_s.shape},Y:{y_s.shape}')
        # hammer.slim(f'返回结果中的x的class是：{fitting_weight.__class__}，shape：{fitting_weight.shape}')
        # 返回的结果，x的shape类似（n，），n代表输入的X矩阵的列数，class为：numpy.ndarray
        ans = {}
        actual_weight = fitting_weight.reshape(1, -1)
        if with_intercept:
            # 这里如果带截距项拟合
            intercept = fitting_weight[0]
            # hammer.slim(f'截距项：{intercept}')
            ans.update({'intercept': intercept})
            # 从返回的x中删除第一个元素，因为这个元素代表了截距
            actual_weight = np.delete(fitting_weight, 0)
            # 这里需要再 reshape，因为如果原数组是一行或者一列的，那么调用这个函数之后，shape变成了（n，）这种长度为1
            # 如果不 reshape，那么和后面的返回结果的维度不一样，引起错误，返回值类型
            # 维度的一致性超级重要
            actual_weight = actual_weight.reshape(1, -1)
        # else:
        #     pass
        # hammer.slim(actual_weight, name='拟合得到的系数')
        ans.update({'weight': actual_weight})

        if cpt_residual_mean or cpt_residual_std or cpt_r_square:
            residual_array = get_z_array(fitting_weight)
            # hammer.slim(residual_array, name='拟合得到的残差矩阵')
            if give_me_residuals:
                ans.update({'residuals': residual_array})
            if cpt_residual_std:
                ans.update({'residual_std': residual_array.std()})
            if cpt_residual_mean:
                ans.update({'residual_mean': residual_array.mean()})
            if cpt_r_square:
                ss_res = get_z(residual_array)
                y_mean = y_s.mean()
                ss_tot = [ii - y_mean for ii in y_s]
                ss_tot = sum([ii[0] * ii[0] for ii in ss_tot])
                r_square = 1 - ss_res / ss_tot
                ans.update({'r_square': r_square})
        return ans
    else:
        LOG.error('优化未完成')
        return
        # raise RuntimeError('优化未完成')


def new_round(_float, _len):
    """
    Parameters
    ----------
    _float: float
    _len: int, 指定四舍五入需要保留的小数点后几位数为_len

    Returns
    -------
    type ==> float, 返回四舍五入后的值
    """
    if isinstance(_float, float):
        if str(_float)[::-1].find('.') <= _len:
            return (_float)
        if str(_float)[-1] == '5':
            return (round(float(str(_float)[:-1] + '6'), _len))
        else:
            return (round(_float, _len))
    else:
        return (round(_float, _len))


def relatively_small(v):
    """
    判断一个值是不是极小，比如很多时候做除法的时候，如果遇到极小值，可能结果超出预期
        如果极小，返回 True；否则，False
    :param v: 被检测值
    :return: bool
    """
    threshold = LOCAL_CONF.getfloat('other', 'relatively_small')
    if math.fabs(v) < threshold:
        LOG.warning(f'找到一个相对较小的值：{v}')
        return True
    else:
        return False


def compute_slope_intercept(x1, y1, x2, y2):
    x_diff = x2 - x1
    slope = (y2 - y1) / x_diff
    intercept = (y1 * x2 - y2 * x1) / x_diff
    return slope, intercept


def compute_y_of_a_line(x, slope, intercept):
    return x * slope + intercept


def compute_x_of_a_line(y, slope, intercept):
    return (y - intercept) / slope


def compute_distance_of_two_points(x1, y1, x2, y2):
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)


def compute_distance_between_a_point_and_a_line(x0, y0, line_info):
    aa = line_info[0]
    bb = -1
    cc = line_info[1]
    numerator = math.sqrt(aa * aa + bb * bb)
    denominator = aa * x0 + bb * y0 + cc
    return math.fabs(denominator / numerator)


if __name__ == '__main__':
    pass
